/*
 
 TopMind jLynx "jLynx JDBC Framework"
 Copyright (c) 2004-2007. TopMind Systems Inc.
 All rights reserved.
 
 This file is part of TopMind jLynx.
 
 TopMind jLynx is free software; you can redistribute it and/or modify
 it under the terms of the License. See website for License.
 
 */
package net.sf.jlynx;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.sql.Connection;
import java.util.Date;
import java.util.Map;
import java.util.Stack;
import java.util.TreeMap;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

/**
 * <p>
 * Parses jLynx configuration file <code>jlynx.xml</code> using SAX XML
 * parser. See website for details.
 * </p>
 * 
 * <p>
 * Revised as of v1.3.2 to check classpath for META-INF/jlynx.xml.
 * </p>
 * 
 * <p>
 * $Id: ConfigParser.java 99 2007-06-21 02:51:13Z gritc11 $
 * </p>
 */
public class ConfigParser extends DefaultHandler implements Serializable {

	protected static final String $version = "v1.3.8-dev (SVN $Revision: 99 $)";

	private static Map _CONN_MAPPING = new TreeMap();

	private static String configFile = null;

	public static final String DEFAULT = "default";

	public static final String DEFAULT_CONFIG_FILENAME = "jlynx.xml";

	static String dir;

	private static String driver;

	protected static String fileSep = System.getProperty("file.separator");

	static boolean isParsed = false;

	private static String jndiDs;

	private static Logger logger = LoggerFactory.getLogger(ConfigParser.class);

	private static Map mappings = new TreeMap();

	private static String password;

	protected static String schema;

	private static final long serialVersionUID = 3246587199368951463L;

	static boolean SingleConnectionMode = true;

	protected static Map NAMED_QUERY_MAPPING = new TreeMap();

	protected static Map QUERY_CONN_MAPPING = new TreeMap();

	protected static Map QUERY_CLASS_MAPPING = new TreeMap();

	private static Stack stack = new Stack();

	private static Map tableMap;

	private static String url;

	private static String username;

	static {
		logger.info("Loading " + ConfigParser.class);
	}

	public static String getConfigFile() {
		return configFile;
	}

	/**
	 * 
	 * Returns a Connection object as configured in jlynx.xml.
	 * 
	 * @param namedConnection -
	 *            connection name from jlynx.xml
	 * @return java.sql.Connection - a valid Connection object
	 * 
	 */
	public static Connection getConnection(String namedConnection) {
		try {
			if (namedConnection == null)
				namedConnection = ConfigParser.DEFAULT;
			if (ConfigParser.getConnectionDefs().containsKey(namedConnection))
				return new TransactionManager().open(namedConnection);
		} catch (Exception e) {
			logger.error("Connection not available :: " + e.getMessage());
		}
		return null;
	}

	static Map getConnectionDefs() {
		return _CONN_MAPPING;
	}

	public static String getDriver() {
		return driver;
	}

	public static String getJndiDs() {
		return jndiDs;
	}

	public static Map getMappings() {
		return mappings;
	}

	protected static String getPassword() {
		return password;
	}

	/**
	 * Returns SQL statement as configured in jlynx.xml.
	 * 
	 * @param namedQuery
	 * @return String - SQL statement
	 */
	public static String getQuery(String namedQuery) {

		String q = (String) ConfigParser.NAMED_QUERY_MAPPING.get(namedQuery);
		if (q == null)
			return "";
		else
			return q;

	}

	public static String getUrl() {
		return url;
	}

	public static String getUsername() {
		return username;
	}

	static void setDriver(String driver) {
		ConfigParser.driver = driver;
	}

	static void setJndiDs(String jndiDs) {
		ConfigParser.jndiDs = jndiDs;
	}

	static void setPassword(String password) {
		ConfigParser.password = password;
	}

	static void setUrl(String url) {
		ConfigParser.url = url;
	}

	static void setUsername(String username) {
		ConfigParser.username = username;
	}

	static void writeConfig(String directory) throws IOException {

		if (directory == null)
			directory = ".";

		dir = directory;
		StringBuffer out = new StringBuffer();
		out.append("<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>\n");
		out.append("<jlynx>\n");
		out.append("<!-- jlynx config generated on " + new Date().toString()
				+ " -->\n");
		out.append(" <connection name=\"" + DEFAULT + "\">\n");

		if (jndiDs != null && !"".equals(jndiDs))
			out.append("  <jndi-ds>" + jndiDs.trim() + "</jndi-ds>\n");
		else
			out.append("  <jndi-ds />\n");

		if (driver != null && !"".equals(driver))
			out.append("  <driver>" + driver.trim() + "</driver>\n");
		else
			out.append("  <driver />\n");

		if (url != null && !"".equals(url))
			out.append("  <db-url>" + url.trim() + "</db-url>\n");
		else
			out.append("  <db-url />\n");

		if (username != null && !"".equals(username))
			out.append("  <db-user>" + username.trim() + "</db-user>\n");
		else
			out.append("  <db-user />\n");

		if (password != null && !"".equals(password))
			out.append("  <password>" + password.trim() + "</password>\n");
		else
			out.append("  <password />\n");

		if (schema != null && !"".equals(schema))
			out.append("  <schema>" + schema.trim() + "</schema>\n");
		else
			out.append("  <schema />\n");

		out.append(Generator.e);

		out.append(" </connection>\n");

		out.append("</jlynx>\n");

		// System.out.println(out);

		File file = new File(directory + File.separator + "jlynx.xml");
		String millis = new Long(System.currentTimeMillis()).toString();
		File fileSave = new File("jlynx-backup-" + millis + ".xml");
		int c;
		if (file.canRead()) {
			FileReader in = new FileReader(file);
			FileWriter writerBak = new FileWriter(fileSave);
			while ((c = in.read()) != -1)
				writerBak.write(c);
			writerBak.close();
		}

		if (file.canWrite() || file.createNewFile()) {
			FileWriter writer = new FileWriter(file);
			writer.write(out.toString());
			writer.flush();
			writer.close();
			System.out.println("Configuration saved to " + directory + fileSep
					+ "jlynx.xml");
		} else
			throw new IOException("cannot write to jlynx.xml");

	}

	private ConnectionBean cb;

	String className = null;

	private boolean connectionValid = true;

	String connName;

	String ds = null;

	private Map entities = null;

	String tableName = null;

	private StringBuffer qry = null;

	private String qryConnection = ConfigParser.DEFAULT;

	private String qryName = null;

	protected ConfigParser() {

		String vendor = (System.getProperty("java.vendor") == null) ? "--"
				: System.getProperty("java.vendor");

		if (!isParsed) {
			logger.info("jLynx initializing : JVM " + vendor + " - "
					+ System.getProperty("java.version"));
		}

		SAXParser sp = null;
		XMLReader xr = null;

		try {

			sp = SAXParserFactory.newInstance().newSAXParser();
			xr = sp.getXMLReader();
			xr.setContentHandler(this);
			InputSource s = new InputSource();
			s.setByteStream(getDefaultConfig());
			xr.parse(s);

		} catch (Exception e) {
			logger.warn(ConfigParser.DEFAULT_CONFIG_FILENAME + " not found");
			isParsed = false;
		}

		// if (isParsed)
		logger.info("jLynx JDBC Framework " + $version
				+ " initialization complete");

	}

	public void characters(char[] chars, int start, int length)
			throws SAXException {

		isParsed = true;

		if (length > 0) {

			String currElement = (String) stack.peek();

			String text = new String(chars, start, length).trim();

			if (text != null && text.length() > 0) {

				if (currElement.equalsIgnoreCase("jndi-ds")) {
					if (cb != null && cb.getName().equals(DEFAULT))
						jndiDs = text;
					cb.setJndiDataSource(text);
				}
				if (currElement.equalsIgnoreCase("db-url")) {
					if (cb != null && cb.getName().equals(DEFAULT))
						url = text;
					// System.out.println(text);
					cb.setUrl(text);
				}
				if (currElement.equalsIgnoreCase("db-user")) {
					if (cb != null && cb.getName().equals(DEFAULT))
						username = text;
					cb.setUsername(text);
				}
				if (currElement.equalsIgnoreCase("driver")) {
					if (cb != null && cb.getName().equals(DEFAULT))
						driver = text;
					cb.setDriver(text);
				}
				if (currElement.equalsIgnoreCase("password")) {
					if (cb != null && cb.getName().equals(DEFAULT))
						password = text;
					cb.setPassword(text);
				}
				if (currElement.equalsIgnoreCase("schema")) {
					if (cb != null && cb.getName().equals(DEFAULT))
						ConfigParser.schema = text;
					cb.setSchema(text);
				}
				if (currElement.equalsIgnoreCase("query")) {

					// saxNormalizeQuery(chars, start, length);
					String str = new String(chars, start, length);
					qry.append(str);

				}

			}

		}
	}

	public void endElement(String namespaceURI, String localName, String qName) {

		if (qName == "connection" && this.connectionValid) {

			cb.setMappings(entities);

			if (!_CONN_MAPPING.containsKey(connName)) {
				_CONN_MAPPING.put(connName, cb);

				logger.info("jLynx Connection - '" + connName + "'");
			} else {
				logger.warn("Cannot add connection named '" + connName
						+ "' because connection already exists");
			}
		} else if ("query".equalsIgnoreCase(qName) && qryName != null
				&& qry != null) {

			if (qry != null) {

				qryName = qryName.trim();

				String q = saxNormalizeQuery(qry.toString());

				if (!ConfigParser.NAMED_QUERY_MAPPING.containsKey(qryName)) {

					ConfigParser.NAMED_QUERY_MAPPING.put(qryName, q);

					logger.info("jLynx NamedQuery - '" + qryName + "' = "
							+ ConfigParser.NAMED_QUERY_MAPPING.get(qryName)
							+ "");
				}

			}
		}

	}

	private InputStream getDefaultConfig() {

		InputStream result = null;
		String config = "META-INF/" + ConfigParser.DEFAULT_CONFIG_FILENAME;
		logger.info("Checking classpath for " + config);

		ClassLoader cl = Thread.currentThread().getContextClassLoader();

		result = cl.getResourceAsStream(config);

		if (result == null) {
			result = ClassLoader.getSystemResourceAsStream(config);
		} else {
			configFile = cl.getResource(config).getFile();
			logger.info("Using resource " + configFile);
			return result;
		}

		if (result != null) {
			configFile = ClassLoader.getSystemResource(config).getFile();
			logger.info("Using system resource " + configFile);
		}

		return result;
	}

	private String saxNormalizeQuery(String input) {

		if (input == null)
			return "";
		else {

			char newLine = '\n';
			char cr = '\r';
			char tab = '\t';
			char space = ' ';

			input = input.replace(newLine, space);
			input = input.replace(tab, space);
			input = input.replace(cr, space);

			while (input.indexOf("  ") != -1)
				input = StringUtils.replace(input, "  ", " ");

			// logger.debug(input);
			return input.trim();
		}

	}

	public void startElement(String uri, String localName, String element,
			Attributes attribs) throws SAXException {

		stack.push(element);

		if ((element.equalsIgnoreCase("entity") || element
				.equalsIgnoreCase("table"))) {

			tableMap = null;

			tableName = attribs.getValue("name");
			className = attribs.getValue("class");
			ds = attribs.getValue("jndi-ds");
			// System.out.println(ds);

			if (className != null && tableName != null) {
				mappings.put(className, tableName);
				entities.put(className, tableName);
			}

			if (ds != null && tableName != null) {
				mappings.put(tableName + ".ds", ds);
			}

		} else if (element.equalsIgnoreCase("column")) {

			String j, d;

			j = attribs.getValue("property");
			d = attribs.getValue("name");

			if (tableMap == null) {
				tableMap = new TreeMap();
			}

			tableMap.put(j, d);

			mappings.put(tableName, tableMap);

			entities.put(tableName, tableMap);

		} else if (element.equalsIgnoreCase("connection")) {

			// stack.pop();
			entities = new TreeMap();

			cb = null;
			cb = new ConnectionBean();
			String cn = attribs.getValue("name");
			if (cn == null) {
				logger
						.error("Config error! Required attribute name is missing i.e. <connection name='?'>");
				this.connectionValid = false;
			} else {
				this.connectionValid = true;
				logger.debug("Parsing connection name = " + cn);
			}
			if (cn != null)
				connName = cn.trim().toLowerCase();

			cb.setName(connName);

			/*
			 * String attrib = attribs.getQName(0); if (attrib != null &&
			 * attrib.equalsIgnoreCase("debug") &&
			 * attribs.getValue(attrib).equalsIgnoreCase("true")) debug = true;
			 */

		} else if (element.equalsIgnoreCase("query")) {

			// process sql name queries
			// format <query name="name" class="Pojo">SELECT ...</query>
			// query should contain valid sql statements used by applications

			if (qryConnection == null)
				qryConnection = ConfigParser.DEFAULT;

			this.qryName = attribs.getValue("name");
			this.qry = new StringBuffer("");

		}

	}

}
